/*
 * $Id: svnclient.c 31 2007-06-20 19:40:30Z john $
 *
 *     SVN Filesystem
 *     Copyright (C) 2006 John Madden <maddenj@skynet.ie>
 *
 *     This program can be distributed under the terms of the GNU GPL.
 *     See the file COPYING for details.
*/

/* vim "+set tabstop=4 shiftwidth=4 expandtab" */

#include "svnfs.h"
#include "svnclient.h"
#include "svn_path.h"

#include <apr_file_io.h>
#include <apr_file_info.h>

/**
 * Callback function to set username and password
 */
svn_error_t *simple_username_password_callback(
                 svn_auth_cred_simple_t **cred, 
                 void *baton, const char *realm, 
                 const char *username, 
                 svn_boolean_t may_save, 
                 apr_pool_t *pool) {

   svn_auth_cred_simple_t *ret = apr_pcalloc(pool,sizeof(*ret));
   if (svnfs.username != NULL) {
      ret->username = svnfs.username;
   }
   if (svnfs.password != NULL) {
      ret->password = svnfs.password; 
   }

   *cred = ret;

   return SVN_NO_ERROR;
}

/**
 * Callback function for untrusted server certificate
 */
static svn_error_t * ssl_trust_unknown_server_cert_callback
  (svn_auth_cred_ssl_server_trust_t **cred_p,
   void *baton,
   const char *realm,
   apr_uint32_t failures,
   const svn_auth_ssl_server_cert_info_t *cert_info,
   svn_boolean_t may_save,
   apr_pool_t *pool)
{
  *cred_p = NULL;

  if (failures == 0 || failures == SVN_AUTH_SSL_UNKNOWNCA)
    {
      *cred_p = apr_pcalloc(pool, sizeof(**cred_p));
      (*cred_p)->may_save = FALSE;
      (*cred_p)->accepted_failures = failures;
      if (failures == SVN_AUTH_SSL_UNKNOWNCA) 
         DEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
         DEBUG("!!!!! Warning, server certificate is untrusted. !!!!!");
         DEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    }

  return SVN_NO_ERROR;
}


int svnclient_setup_ctx() {
    svn_auth_baton_t *auth_baton;
    apr_array_header_t *providers;
    svn_auth_provider_object_t *username_password_provider;
    svn_auth_provider_object_t *server_trust_provider;
    svn_error_t *err;
    char errbuf[1024];

    /* Initialise a pool */
    apr_initialize();
    pool = svn_pool_create(NULL);

    /* Initialize attribute and list hash map */
    dirbuf_hash = g_hash_table_new(g_str_hash,g_str_equal);
    pthread_mutex_init(&lock_dirbuf_hash,NULL);
    listbuf_hash = g_hash_table_new(g_str_hash,g_str_equal);
    pthread_mutex_init(&lock_listbuf_hash,NULL);

    /* Initialise errbuf */
    memset(&errbuf, '\0', 1024);

    /* Create a client context object */
    if( (err = svn_client_create_context(&ctx, pool)) ) {
        fprintf(stderr, "%s\n", svn_strerror(err->apr_err, errbuf, 1024));
        return(1);
    }

    if( (err = (svn_error_t *)svn_config_get_config(
                    &(ctx->config), NULL, pool)) ) {
        fprintf(stderr, "%s\n", svn_strerror(err->apr_err, errbuf, 1024));
        return(1);
    }

    providers = apr_array_make(pool, 1, sizeof(svn_auth_provider_object_t *));
    username_password_provider = apr_pcalloc(pool, sizeof(*username_password_provider));
//    svn_client_get_username_provider(&username_password_provider, pool);
    svn_client_get_simple_prompt_provider(&username_password_provider, 
            simple_username_password_callback,NULL,0,pool);
    *(svn_auth_provider_object_t **)apr_array_push(providers) 
        = username_password_provider;
    
    if (svnfs.untrusted) {
       // ignore untrusted server certificate.
       server_trust_provider = apr_pcalloc(pool, sizeof(*server_trust_provider));
       svn_auth_get_ssl_server_trust_prompt_provider(&server_trust_provider,
            ssl_trust_unknown_server_cert_callback,NULL,pool);
       *(svn_auth_provider_object_t **)apr_array_push(providers) 
           = server_trust_provider;

    }

    svn_auth_open(&auth_baton, providers, pool);
    ctx->auth_baton = auth_baton;

    return(0);
}

/* Information of the current directory is passed in 'baton', while the directory entry is passed in 'path' 
 */
static svn_error_t *_svnclient_list_func(void *baton, const char *path,
        const svn_dirent_t *dirent, const svn_lock_t *lock,
        const char *abs_path, apr_pool_t *pool) {
    char *fullpath;
    struct listbuf *lp = (struct listbuf *)baton; 

    /* The first 'path' argument is blank, meaning it's the details for
     * lp->path. If path length is 0 and lp->path length is 1, then
     * we're listing the '/' of the filesystem, for which a static
     * dirent is returned from svnfs_getattr(), so skip it */
    if( (strlen(path) == 0) && (strlen(lp->path) == 1) ) {
        if (svnfs.revision == 0) {
            svnfs.revision = dirent->created_rev; // Fix the current head revision for consistency
            DEBUG("_svnclient_list_func(): detect revision %d", svnfs.revision);
        }
        return(SVN_NO_ERROR);
    }
    DEBUG("_svnclient_list_func(): '%s' '%s'", lp->path,path);

    /* path is a file under the directory */
    if( (strlen(path) > 0) && (strlen(lp->path) > 1) ) {
        if( (fullpath = malloc((sizeof(char)) * 
                        (strlen(lp->path) + strlen(path) + 2))) == NULL )
            return(svn_error_create(SVN_ERR_FS_GENERAL, NULL, strerror(errno)));
        sprintf(fullpath, "%s/%s", lp->path, path);
    } else {
        if( (fullpath = malloc((sizeof(char)) * 
                        (strlen(lp->path) + strlen(path) + 1))) == NULL )
            return(svn_error_create(SVN_ERR_FS_GENERAL, NULL, strerror(errno)));
        sprintf(fullpath, "%s%s", lp->path, path);
    }

    DEBUG("_svnclient_list_func(): path %s", fullpath);

    pthread_mutex_lock(&lock_dirbuf_hash);
    struct dirbuf *dp = g_hash_table_lookup(dirbuf_hash,fullpath); 
    pthread_mutex_unlock(&lock_dirbuf_hash);
    if (dp == NULL) {
       dp = malloc(sizeof(struct dirbuf));
       dp->name = strdup(fullpath);
       if( dirent->kind == svn_node_file )
           dp->st.st_mode = S_IFREG | 0644;
       else
           dp->st.st_mode = S_IFDIR | 0755;

       dp->st.st_size = dirent->size;
       dp->st.st_mtime = apr_time_sec(dirent->time);
       dp->rev = dirent->created_rev;
       pthread_mutex_init(&(dp->lock),NULL);
       pthread_mutex_lock(&lock_dirbuf_hash);
       g_hash_table_insert(dirbuf_hash,dp->name,dp);
       pthread_mutex_unlock(&lock_dirbuf_hash);
       DEBUG("_svnclient_list_func(): put '%s' revision %d into attribute cache", dp->name, dp->rev);
    }
    if (strlen(path) > 0) {
       lp->list = g_slist_append(lp->list,dp);
    }

    //DEBUG("_svnclient_list_func(): lp->list size '%d'",g_slist_length(lp->list));
    free(fullpath);
    return(SVN_NO_ERROR);
}

/* If a file isn't contained in the list cache, this will get called. 
 *  *dp is unused now.
 * Both file and directory will be cached in list cache.
 * We should let only directory be cached. Maybe, we should separate  
 *  svnclient_list() for getattr() and readdir().
 */
int svnclient_list(const char *path, struct dirbuf *dp) {
    svn_opt_revision_t *rev;
    apr_pool_t *subpool = svn_pool_create(pool);
    apr_uint32_t dirent_fields = SVN_DIRENT_KIND | SVN_DIRENT_SIZE | SVN_DIRENT_CREATED_REV | SVN_DIRENT_TIME;
    svn_error_t *err = NULL;
    struct listbuf *lp;

    char *fullpath;

    lp = malloc(sizeof(struct listbuf));
    lp->path = strdup(path);
    lp->list = NULL;

    rev = apr_pcalloc(subpool,sizeof(*rev));
    if (svnfs.revision > 0) {
       rev->kind = svn_opt_revision_number;
       rev->value.number = svnfs.revision;
    } else {
       rev->kind = svn_opt_revision_head;
    }

    if( (fullpath = apr_pcalloc(subpool,strlen(svnfs.svnpath) + strlen(path) + 1)) == NULL )
        return(EIO);
    sprintf(fullpath, "%s%s", svnfs.svnpath, path);
    while( fullpath[strlen(fullpath)-1] == '/' )
        fullpath[strlen(fullpath)-1] = '\0';

    DEBUG("svnclient_list(): '%s'", fullpath);

    pthread_mutex_lock(&lock_listbuf_hash);
    GSList *list = g_hash_table_lookup(listbuf_hash,fullpath); 
    pthread_mutex_unlock(&lock_listbuf_hash);
    if (list != NULL) {
        DEBUG("svnclient_list(): found '%s' in list cache", fullpath);
        apr_pool_destroy(subpool);
        return(0);
    }

    char *encoded_fullpath = svn_path_uri_encode(fullpath,subpool); // Bug! Allocating memory from subpool may cause svn_client_cat2 to fail, if put this line just before svn_client_cat2() below. Don't now why.

    if( (err = svn_client_list(encoded_fullpath, rev, rev, FALSE, dirent_fields, FALSE,
                _svnclient_list_func, (void *)lp, ctx, subpool)) 
            != SVN_NO_ERROR ) {
        DEBUG("svnclient_list() returns error: %s",err->message);
        apr_pool_destroy(subpool);
        switch(err->apr_err) {
            case SVN_ERR_FS_NOT_FOUND:
                return(ENOENT);
            default:
                return(EIO);
        }
    }
    
    pthread_mutex_lock(&lock_listbuf_hash);
    g_hash_table_insert(listbuf_hash,lp->path,lp);
    pthread_mutex_unlock(&lock_listbuf_hash);
    DEBUG("svnclient_list(): add '%s' into list cache", path);

    apr_pool_destroy(subpool);
    return(0);
}

int svnclient_read(struct dirbuf *dp, char *buf, size_t *size, off_t offset) {
    svn_opt_revision_t *rev;
    svn_stream_t *out;
    svn_stringbuf_t *sbuf;
    char *path;
    apr_pool_t *subpool = svn_pool_create(pool);
    svn_error_t *err = NULL;
    char *errbuf;

    sbuf = svn_stringbuf_create("", subpool);
    out = svn_stream_from_stringbuf(sbuf, subpool);

    path = apr_pcalloc(subpool,strlen(svnfs.svnpath) + strlen(dp->name) + 1);
    sprintf(path, "%s%s", svnfs.svnpath, dp->name);

    rev = apr_pcalloc(subpool,sizeof(rev));
    rev->kind = svn_opt_revision_head;

    if( (err = svn_client_cat2(out, path, rev, rev, ctx, subpool))            == SVN_NO_ERROR ) {        
        if( sbuf->len < *size ) {
            memcpy(buf, sbuf->data + offset, sbuf->len);
            *size = sbuf->len;
        } else {
            memcpy(buf, sbuf->data + offset, *size);
        }
        DEBUG("svnclient_read(): size %ld", sbuf->len);    
    } else {
        errbuf = malloc(1024);
        DEBUG("svnclient_read(): svn_client_cat() - %s",
                svn_strerror(err->apr_err, errbuf, 1024));
        free(errbuf);

        apr_pool_destroy(subpool);
        switch(err->apr_err) {
            case SVN_ERR_UNVERSIONED_RESOURCE:
            case SVN_ERR_ENTRY_NOT_FOUND:
                return(EEXIST);
            case SVN_ERR_CLIENT_IS_DIRECTORY:
                return(EISDIR);
            default:
                return(EIO);
        }    
    }

    apr_pool_destroy(subpool);
    return(0);
}

int svnclient_cache(struct dirbuf *dp, const char *cache_id) {
    svn_opt_revision_t *rev;
    svn_stream_t *out;
    char *path;
    char *cache_tmp;
    apr_pool_t *subpool = svn_pool_create(pool);
    svn_error_t *err = NULL;
    char *errbuf;

    apr_file_t  *fi;
    apr_status_t rc;

    path = apr_pcalloc(subpool,strlen(svnfs.svnpath) + strlen(dp->name) + 1);
    sprintf(path, "%s%s", svnfs.svnpath, dp->name);

    char *encoded_path = svn_path_uri_encode(path,subpool); // Bug! Allocating memory from subpool causes svn_client_cat2 to fail, if put this line just before svn_client_cat2() below. Don't now why.

    cache_tmp = apr_pcalloc(subpool,strlen(cache_id) + 6);
    sprintf(cache_tmp, "%s%s", cache_id, ".part");

    if( (rc = apr_file_open(&fi,cache_tmp,APR_WRITE | APR_CREATE,APR_UREAD|APR_UWRITE,subpool)) != APR_SUCCESS) {
       return(EIO);
    }
    out = svn_stream_from_aprfile(fi, subpool);

    rev = apr_pcalloc(subpool,sizeof(rev));
    rev->kind = svn_opt_revision_number;
    rev->value.number = dp->rev;

    DEBUG("svnclient_read_cached(): start caching %s",cache_id);

    if( (err = svn_client_cat2(out, encoded_path, rev, rev, ctx, subpool)) 
            == SVN_NO_ERROR ) {
        apr_file_close(fi);
        if ( (err = rename(cache_tmp,cache_id)) != 0 ) {
           DEBUG("svnclient_read_cached(): fail caching %s",cache_id);
           apr_pool_destroy(subpool);
           return(err);
        }
        DEBUG("svnclient_read_cached(): finish caching %s",cache_id);

    } else {
        apr_file_close(fi);
        errbuf = malloc(1024);
        DEBUG("svnclient_read(): svn_client_cat() - %s", 
                svn_strerror(err->apr_err, errbuf, 1024));
        free(errbuf);

        apr_pool_destroy(subpool);
        switch(err->apr_err) {
            case SVN_ERR_UNVERSIONED_RESOURCE:
            case SVN_ERR_ENTRY_NOT_FOUND:
                return(EEXIST);
            case SVN_ERR_CLIENT_IS_DIRECTORY:
                return(EISDIR);
            default:
                return(EIO);
        }
    }

    apr_pool_destroy(subpool);

    return(0);
}
